local redis = require('resty.redis')
local utils = require('modules.commhm.utils')
local timer = require("modules.commhm.ngxTimerWrap")
local log   = require('mis_log')

local ngx = ngx
local type = type
local pairs = pairs
local debug = debug
local unpack = unpack
local string = string
local tonumber = tonumber
local setmetatable = setmetatable

redis.add_commands('hkeys', 'lrem', 'setnx', 'rpush', 'hsetnx')

local this = {
    manager = {},
    module  = string.match(debug.getinfo(1).short_src, '.*/(.*).lua'), -- 模块名称

    pool_size = 100,
    max_idle_timeout = 60000,
}

local list_to_map = function( list )
    local result = {}
    for _, member in pairs(list or {}) do
        result[member] = 1
    end
    return result
end

local checkCDKeys = function (CDKeys)
    if type(CDKeys) ~= "table" then
        return false, 'not table'
    end
    for _, code in pairs(CDKeys) do
        if type(code) ~= 'string' then
            return false, code .. 'not string'
        end
    end
    return true
end

local table_len = function (tab)
    local total = 0
    for _, value in pairs(tab or {}) do
        total = total + 1
    end
    return total
end

function this:log( s_msg, t_data )
    log.day(self.module, utils.to_str({
        worker = ngx.worker.id(),
        work   = self.name,
        msg    = s_msg,
        data   = t_data
    }))
end

function this:redisinst()
    ngx.ctx.redis = ngx.ctx.redis or {}
    if not ngx.ctx.redis[this.module] then
        -- 新建实例
        local rdb = redis:new()
        local ok, err = rdb:connect(self.connect.ip, self.connect.port)
        if not ok then
            self:log('redis:connect fail',{
                connect = self.connect,
                error   = err,
            })
            return nil, 'redis connect fault'
        end

        if 0 == rdb:get_reused_times() then
            ok, err = rdb:auth(self.connect.auth)
            if not ok then
                rdb:set_keepalive(this.max_idle_timeout, this.pool_size)
                self:log('redis:auth  fail',{
                    connect = self.connect,
                    error   = err,
                })
                return nil, 'redis auth fault'
            end
        end
        ngx.ctx.redis[this.module] = rdb
    end

    return ngx.ctx.redis[this.module]
end

function this:redisLLen(lkey)
    local rdb, dsc = self:redisinst()
    if not rdb then
        self:log('redis:llen inst fail', {
            key = lkey,
            err = dsc,
        })
        return nil
    end
    local res, err = rdb:llen(lkey)
    if err then
        self:log('redis:llen fail', {
            key = lkey,
            err = err,
        })
        return nil
    end
    return res
end

function this:redisHLen(hkey)
    local rdb, dsc = self:redisinst()
    if not rdb then
        self:log('redis:llen inst fail', {
            key = hkey,
            err = dsc,
        })
        return nil
    end
    local res, err = rdb:hlen(hkey)
    if err then
        self:log('redis:hlen fail', {
            key = hkey,
            err = err,
        })
        return nil
    end
    return res
end

function this:redisLPop( uin )
    local rdb, dsc = self:redisinst()
    if not rdb then
        self:log('redis:lpop inst fail', {
            key = self.free,
            err = dsc,
        })
        return nil
    end

    local res, err = rdb:lpop(self.free)
    if err then
        self:log('redis:lpop fail', {
            key = self.free,
            err = err,
        })
        return nil
    end

    local result = {}
    local cdkeys = type(res) == 'string' and { [1] = res } or res
    if cdkeys == ngx.null then
        self:log("lpop null", { res = res })
        return nil, true
    end

    for _, cdkey in pairs(cdkeys or {}) do
        res, err = rdb:hsetnx(self.sold, cdkey, uin)
        if res == 1 then
            result[#result+1] = cdkey

            self:log('redis:hsetnx succ', {
                cdkey = cdkey,
                uin   = uin,
            })
        end
    end

    return utils.tableNotEmpty(result) and result or nil
end

function this:redisUnLock()
    local rdb, dsc = self:redisinst()
    if not rdb then
        self:log('redisUnLock inst fail', {
            key = self.free,
            err = dsc,
        })
        return
    end
    local res, err = rdb:del(self.lock)
    if err or res ~= 1 then
        self:log('redisUnLock fail', {
            lock = self.lock,
            err = err
        })
    end
end

-- function this:redisLock()
--     local rdb, dsc = self:redisinst()
--     if not rdb then
--         self:log('redisLock inst fail', {
--             key = self.free,
--             err = dsc,
--         })
--         return false
--     end

--     rdb:init_pipeline()
--     rdb:setnx(self.lock, 1)
--     rdb:expire(self.lock, 30)
--     local res, err = rdb:commit_pipeline()

--     --local setnxRet = res[1]
--     --if err or setnxRet ~= 1 then
--     if err then
--         self:log('redisLock fail1', {
--             lock = self.lock,
--             res  = res,
--             err  = err,
--         })
--         return false
--     end

--     local expireRet = res[2]
--     if expireRet ~= 1 then
--         -- 分布式锁设置过期失败直接解锁返回失败
--         self:log('redisLock fail2', {
--             lock = self.lock,
--             res  = res
--         })
--         self:redisUnLock()
--         return false
--     end

--     return true
-- end

function this:redisLock()
    local rdb, dsc = self:redisinst()
    if not rdb then
        self:log('redisLock inst fail', {
            key = self.free,
            err = dsc,
        })
        return false
    end

    local res, err = rdb:set(self.lock, 1, "ex", 30, "nx")
    if err or res ~= 'OK' then
        self:log('redisLock fail1', {
            lock = self.lock,
            res  = res,
            err  = err,
        })
        return false
    end

    return true
end

function this:redisRepoAll( )
    local rdb, dsc = self:redisinst()
    if not rdb then
        self:log('redisRepoAll inst fail', {
            key = self.free,
            err = dsc,
        })
        return false
    end
    rdb:init_pipeline()
    rdb:lrange(self.free, 0, -1)
    rdb:hkeys(self.sold)
    local res, err = rdb:commit_pipeline()
    if err or not res then
        self:log('redisRepoAll fail', {
            free = self.free,
            sold = self.sold,
            res  = res,
            err  = err
        })
        return false
    end

    local free = list_to_map(res[1] or {})
    local sold = list_to_map(res[2] or {})
    -- self:log('redisRepoAll debug', {
    --     free = free,
    --     sold = sold,
    --     res  = res,
    --     err  = err
    -- })
    return true, free, sold
end

function this:redisLRemX( delList )
    local rdb, dsc = self:redisinst()
    if not rdb then
        self:log('redisLRemX inst fail', {
            key = self.free,
            err = dsc,
        })
        return 0
    end
    rdb:init_pipeline()
    -- 仅删除未发货
    for _, cdkey in pairs(delList or {}) do
        rdb:lrem(self.free, 0, cdkey)
    end
    local res, err = rdb:commit_pipeline()
    self:log('redisLRemX', {
        delList = delList,
        res = res,
        err = err
    })

    -- 统计删除成功个数
    local count = 0
    for _, code in pairs(res or {}) do
        if code == 1 then
            count = count + 1
        end
    end
    return count
end

function this:redisRPush( addList )
    local rdb, dsc = self:redisinst()
    if not rdb then
        self:log('redisRPush inst fail', {
            key = self.free,
            err = dsc,
        })
        return 0
    end
    rdb:init_pipeline()
    rdb:rpush(self.free, unpack(addList))

    local res, err = rdb:commit_pipeline()
    if err then
        self:log('redisRPush fail', {
            free    = self.free,
            addList = addList,
            err     = err,
        })
        return 0
    end
    self:log('redisRPush succ', {
        addList = addList,
        result  = res
    })

    -- 统计成功增加个数
    return res[1]
end

function this:cdkeyReload( CDKeys )
    local ok, desc = checkCDKeys(CDKeys)
    if not ok then
        return false, desc
    end

    if not self:redisLock() then
        return false, 'redislock fail'
    end

    local addList, delList = {}, {}
    local config = list_to_map(CDKeys)
    local loaded, free, sold = self:redisRepoAll()
    if not (loaded and free and sold) then
        self:redisUnLock()
        return false
    end

    for cdkey, _ in pairs(config) do
        if not free[cdkey] and not sold[cdkey] then
            addList[#addList+1] = cdkey
        end
    end
    for cdkey, _ in pairs(free) do
        if not config[cdkey] then
            delList[#delList+1] = cdkey
        end
    end

    local del_cnt = (#delList > 0) and self:redisLRemX(delList) or 0
    if #addList > 0 then
        local add_cnt = self:redisRPush(addList)
        self.remain = add_cnt - del_cnt
        self.total  = self.remain + table_len(sold)
    else
        self.remain = table_len(free) - del_cnt
        self.total  = self.remain + table_len(sold)
    end

    self:redisUnLock()
    return true
end

function this:configInit( config )
    self.connect = self.connect or {}
    self.connect.ip   = config.ip
    self.connect.port = config.port
    self.connect.auth = config.auth
end

function this:loadCDKeys( CDKeys )
    if ngx.worker.id() == 0 then

        -- 0号worker处理redis数据初始化
        local ok, desc = self:cdkeyReload(CDKeys)
        if not ok then
            return false, 'cdkey init fail ' .. desc
        end

        self:log('cdkey init finish', {
            loaded = #CDKeys,
            total  = self.total,
            remain = self.remain
        })
    end

    self.remain = tonumber(self:redisLLen(self.free)) or self.remain
    self.total  = self.remain + (tonumber(self:redisHLen(self.sold)) or 0)
    self.empty  = self.remain <= 0
    self:log('load cdkey finish', {
        loaded = #CDKeys,
        total  = self.total,
        remain = self.remain
    })

    return true
end

function this:start_timer()
    -- 当前无库存或者已有定时器
    if self.timer then
        return true
    end

    self.timer = timer:new()
    local _, s_err = self.timer:ngx_timer_every(10,
    function ()
        self.remain = tonumber(self:redisLLen(self.free)) or self.remain
        self.total  = self.remain + (tonumber(self:redisHLen(self.sold)) or 0)
        self.empty  = self.remain <= 0
    end)
    if s_err then
        return false, 'timer init fail :' .. s_err
    end

    self:log('timer start')
    return true
end

function this:init( config, cdkeys )
    -- 1. redis 配置
    self:configInit(config)

    -- 2. 加载 CDKey
    self:loadCDKeys(cdkeys)

    -- 3. 定时刷新剩余票数
    local ok, desc = self:start_timer()
    if not ok then
        return false, desc
    end
    return true
end

function this:reload( cdkeys )
    local ok, desc = self:loadCDKeys(cdkeys)
    if not ok then
        -- 新配置加载失败
        self:log('reload fail', desc)
    else
        self:log("relod succ")
    end
end

function this:new(work, config, cdkeys)
    local name = this.module .. '_' .. work
    local obj  = setmetatable({
        name = name,
        -- 使用hash tag模式兼容集群模式
        lock = '{' .. name .. '}_lock', -- 锁
        free = name .. '_free',     -- 未售
        sold = name .. '_sold',     -- 已售
    }, {__index = this})
    local ok, desc = obj:init(config, cdkeys)
    if not ok then
        return nil, 'init fail : ' .. desc
    end
    return obj
end

function this:inst(work)
    return  this.manager[work]
end

function this:register(work, config, cdkeys)
    local ok, desc = checkCDKeys(cdkeys)
    if not ok then
        return nil, 'invalid cdkey list : ' .. desc
    end

    local obj = this:inst(work)
    if not obj then
        -- 首次启动
        obj, desc = this:new(work, config, cdkeys)
        if not obj then
            return nil, desc
        end
        this.manager[work] = obj
    else
        -- 配置热更了
        obj:reload(cdkeys)
    end
    return obj
end

-- return : cdkey, is_empty
function this:Get( uin )
    if self.empty then
        return nil, true
    end

    local cdkey, empty = self:redisLPop(uin)
    if not cdkey and empty then
        self:log("all selled", { cdkey = cdkey or "nil", empty = empty })
        self.empty = true
    end

    return cdkey
end

return this